server-side-props.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
  2. import type {
  3. IDataWithMeta,
  4. IDataWithRequiredMeta,
  5. IPageInfoBasic,
  6. IPageNotFoundInfo,
  7. } from '@growi/core';
  8. import { isIPageNotFoundInfo } from '@growi/core';
  9. import { pagePathUtils } from '@growi/core/dist/utils';
  10. import {
  11. SupportedAction,
  12. type SupportedActionType,
  13. } from '~/interfaces/activity';
  14. import type { CrowiRequest } from '~/interfaces/crowi-request';
  15. import type { PageDocument } from '~/server/models/page';
  16. import { getServerSideBasicLayoutProps } from '../basic-layout-page';
  17. import {
  18. getServerSideCommonInitialProps,
  19. getServerSideI18nProps,
  20. } from '../common-props';
  21. import {
  22. getServerSideGeneralPageProps,
  23. getServerSideRendererConfigProps,
  24. } from '../general-page';
  25. import { isValidGeneralPageInitialProps } from '../general-page/type-guards';
  26. import type { IPageToShowRevisionWithMeta } from '../general-page/types';
  27. import { addActivity } from '../utils/activity';
  28. import { mergeGetServerSidePropsResults } from '../utils/server-side-props';
  29. import { NEXT_JS_ROUTING_PAGE } from './consts';
  30. import {
  31. getPageDataForInitial,
  32. getPageDataForSameRoute,
  33. } from './page-data-props';
  34. import type { Stage2EachProps, Stage2InitialProps } from './types';
  35. const nextjsRoutingProps = {
  36. props: {
  37. nextjsRoutingPage: NEXT_JS_ROUTING_PAGE,
  38. },
  39. };
  40. /**
  41. * Emit page seen event
  42. * @param context - Next.js server-side context
  43. * @param pageId - Page ID to mark as seen
  44. */
  45. function emitPageSeenEvent(
  46. context: GetServerSidePropsContext,
  47. pageId?: string,
  48. ): void {
  49. if (pageId == null) {
  50. return;
  51. }
  52. const req = context.req as CrowiRequest;
  53. const { user, crowi } = req;
  54. if (user == null) {
  55. return;
  56. }
  57. const pageEvent = crowi.event('page');
  58. pageEvent.emit('seen', pageId, user);
  59. }
  60. function getActivityAction(
  61. isIdenticalPathPage: boolean,
  62. pageWithMeta?:
  63. | IPageToShowRevisionWithMeta
  64. | IDataWithRequiredMeta<PageDocument, IPageInfoBasic>
  65. | IDataWithMeta<null, IPageNotFoundInfo>
  66. | null,
  67. ): SupportedActionType {
  68. if (isIdenticalPathPage) {
  69. return SupportedAction.ACTION_PAGE_NOT_CREATABLE;
  70. }
  71. const meta = pageWithMeta?.meta;
  72. if (isIPageNotFoundInfo(meta)) {
  73. if (meta.isForbidden) {
  74. return SupportedAction.ACTION_PAGE_FORBIDDEN;
  75. }
  76. if (meta.isNotFound) {
  77. return SupportedAction.ACTION_PAGE_NOT_FOUND;
  78. }
  79. }
  80. const pagePath = pageWithMeta?.data?.path;
  81. if (pagePath != null) {
  82. if (pagePathUtils.isUsersHomepage(pagePath)) {
  83. return SupportedAction.ACTION_PAGE_USER_HOME_VIEW;
  84. }
  85. if (!pagePathUtils.isCreatablePage(pagePath)) {
  86. return SupportedAction.ACTION_PAGE_NOT_CREATABLE;
  87. }
  88. }
  89. return SupportedAction.ACTION_PAGE_VIEW;
  90. }
  91. export async function getServerSidePropsForInitial(
  92. context: GetServerSidePropsContext,
  93. ): Promise<GetServerSidePropsResult<Stage2InitialProps>> {
  94. const [
  95. commonInitialResult,
  96. basicLayoutResult,
  97. generalPageResult,
  98. rendererConfigResult,
  99. i18nPropsResult,
  100. pageDataResult,
  101. ] = await Promise.all([
  102. getServerSideCommonInitialProps(context),
  103. getServerSideBasicLayoutProps(context),
  104. getServerSideGeneralPageProps(context),
  105. getServerSideRendererConfigProps(context),
  106. getServerSideI18nProps(context, ['translation']),
  107. getPageDataForInitial(context),
  108. ]);
  109. // Merge all results in a type-safe manner (using sequential merging)
  110. const mergedResult = mergeGetServerSidePropsResults(
  111. commonInitialResult,
  112. mergeGetServerSidePropsResults(
  113. basicLayoutResult,
  114. mergeGetServerSidePropsResults(
  115. generalPageResult,
  116. mergeGetServerSidePropsResults(
  117. rendererConfigResult,
  118. mergeGetServerSidePropsResults(
  119. i18nPropsResult,
  120. mergeGetServerSidePropsResults(pageDataResult, nextjsRoutingProps),
  121. ),
  122. ),
  123. ),
  124. ),
  125. );
  126. // Check for early return (redirect/notFound)
  127. if ('redirect' in mergedResult || 'notFound' in mergedResult) {
  128. return mergedResult;
  129. }
  130. const mergedProps = await mergedResult.props;
  131. // Type-safe props validation AFTER skipSSR is properly set
  132. if (!isValidGeneralPageInitialProps(mergedProps)) {
  133. throw new Error('Invalid merged props structure');
  134. }
  135. // Add user to seen users
  136. emitPageSeenEvent(context, mergedProps.pageWithMeta?.data?._id);
  137. // Persist activity
  138. addActivity(
  139. context,
  140. getActivityAction(
  141. mergedProps.isIdenticalPathPage,
  142. mergedProps.pageWithMeta,
  143. ),
  144. );
  145. return mergedResult;
  146. }
  147. export async function getServerSidePropsForSameRoute(
  148. context: GetServerSidePropsContext,
  149. ): Promise<GetServerSidePropsResult<Stage2EachProps>> {
  150. // -- TODO: :https://redmine.weseek.co.jp/issues/174725
  151. // Remove getServerSideI18nProps from getServerSidePropsForSameRoute for performance improvement
  152. const [i18nPropsResult, pageDataForSameRouteResult] = await Promise.all([
  153. getServerSideI18nProps(context, ['translation']),
  154. getPageDataForSameRoute(context),
  155. ]);
  156. const { props: pageDataProps, internalProps } = pageDataForSameRouteResult;
  157. // Add user to seen users
  158. emitPageSeenEvent(
  159. context,
  160. internalProps?.pageWithMeta?.data?._id?.toString(),
  161. );
  162. // Persist activity
  163. addActivity(
  164. context,
  165. getActivityAction(
  166. pageDataProps.isIdenticalPathPage,
  167. internalProps?.pageWithMeta,
  168. ),
  169. );
  170. const mergedResult = mergeGetServerSidePropsResults(
  171. { props: pageDataProps },
  172. i18nPropsResult,
  173. );
  174. return mergedResult;
  175. }